package cn.kevinkun.Box2D;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.jbox2d.collision.shapes.CircleShape;
import org.jbox2d.collision.shapes.EdgeShape;
import org.jbox2d.collision.shapes.PolygonShape;
import org.jbox2d.common.Vec2;
import org.jbox2d.dynamics.Body;
import org.jbox2d.dynamics.BodyDef;
import org.jbox2d.dynamics.BodyType;
import org.jbox2d.dynamics.FixtureDef;

import com.google.appinventor.components.annotations.DesignerComponent;
import com.google.appinventor.components.annotations.SimpleFunction;
import com.google.appinventor.components.annotations.SimpleObject;
import com.google.appinventor.components.annotations.SimpleProperty;
import com.google.appinventor.components.annotations.UsesAssets;
import com.google.appinventor.components.annotations.UsesLibraries;
import com.google.appinventor.components.common.ComponentCategory;
import com.google.appinventor.components.runtime.AndroidNonvisibleComponent;
import com.google.appinventor.components.runtime.ComponentContainer;
import com.google.appinventor.components.runtime.errors.YailRuntimeError;
import com.google.appinventor.components.runtime.util.YailList;

import android.widget.ImageView;

@DesignerComponent(version = 1, //
		category = ComponentCategory.EXTENSION, //
		description = "Manipulate body in the boxWorld. You need to connect this with boxWorld by call SetBoxWorld as first step. ", //
		iconName = "aiwebres/logo.jpg", //
		nonVisible = true, //
		helpUrl = "https://kevinkun.cn/box2d")
@UsesAssets(fileNames = "")
@UsesLibraries(libraries = "")
@SimpleObject(external = true)
public class BoxBody extends AndroidNonvisibleComponent {
	BoxWorld boxWorld;

	private float friction = 0.3f;
	private float density = 3.0f;
	private float restitution = 0.5f;
	private float linearDamping = 0.3f;
	private float angularDamping = 0.3f;

	public BoxBody(ComponentContainer container) {
		super(container.$form());
	}

	@SimpleFunction
	public void SetBoxWorld(BoxWorld boxWorld) {
		this.boxWorld = boxWorld;
	}

	@SimpleFunction(description = "type: 1 for dynamic, 2 for static, 3 for kinematic")
	public Body CreateCircle(String image, int centerX, int centerY, int radius, int type) throws IOException {
		assertWorld();
		Body body = createCircleBody(centerX, centerY, radius, type);
		ImageView imageView = boxWorld.addImageView(centerX - radius, centerY - radius, radius * 2, radius * 2, image);
		body.setUserData(imageView);
		imageView.setTag(body);
		return body;
	}

	@SimpleFunction(description = "type: 1 for dynamic, 2 for static, 3 for kinematic")
	public Body CreateRectangle(String image, int left, int top, int width, int height, int type) throws IOException {
		assertWorld();
		Body body = createRectangleBody(left, top, width, height, type);
		ImageView imageView = boxWorld.addImageView(left, top, width, height, image);
		body.setUserData(imageView);
		imageView.setTag(body);

		return body;
	}

	@SimpleFunction(description = "points, a point list in ccw order. \\ntype: 1 for dynamic, 2 for static, 3 for kinematic")
	public Body CreatePolygon(String image, int centerX, int centerY, YailList points, int type) throws IOException {
		assertWorld();
		if (points.size() > 8) {
			throw new YailRuntimeError("Polygon sides no more than 8", "CreatePolygon");
		}
		float[][] pts = BoxUtil.parsePointList(points);
		List<Integer> xVal = new ArrayList<>();
		List<Integer> yVal = new ArrayList<>();
		float sumx = 0, sumy = 0;

		for (int i = 0; i < pts.length; i++) {
			xVal.add((int) pts[i][0]);
			yVal.add((int) pts[i][1]);
			sumx += pts[i][0];
			sumy += pts[i][1];
		}
		int left = Collections.min(xVal);
		int top = Collections.min(yVal);
		int right = Collections.max(xVal);
		int bottom = Collections.max(yVal);

		Body body = createPolygonBody(centerX, centerY, pts, right - left, bottom - top, type);

		ImageView imageView = boxWorld.addImageView(left, top, right - left, bottom - top, image);
		body.setUserData(imageView);
		imageView.setTag(body);

		return body;
	}

	@SimpleFunction
	public Body CreateEdge(float x1, float y1, float x2, float y2) {
		assertWorld();
		BodyDef bd = new BodyDef();
		bd.type = BodyType.STATIC;
		Body edge = boxWorld.getWorld().createBody(bd);

		EdgeShape shape = new EdgeShape();
		shape.set(BoxUtil.pixelToVec2(x1, y1), BoxUtil.pixelToVec2(x2, y2));
		FixtureDef fd = new FixtureDef();
		fd.shape = shape;
		edge.createFixture(fd);
		return edge;
	}

	@SimpleFunction
	public void DestroyBody(Body body) {
		assertWorld();
		boxWorld.bodyToDestroy.add(body);
	}

	@SimpleFunction
	public void ApplyLinearImpulse(Body body, float vecX, float vecY) {
		Vec2 impulse = new Vec2(vecX, vecY);
		body.applyLinearImpulse(impulse, body.getPosition());
	}

	@SimpleFunction
	public void ApplyLinearForce(Body body, float vecX, float vecY) {
		Vec2 impulse = new Vec2(vecX, vecY);
		body.applyForceToCenter(impulse);
	}

	@SimpleFunction
	public void SetLinearVelocity(Body body, float vecX, float vecY) {
		body.setLinearVelocity(new Vec2(vecX, vecY));
	}

	@SimpleFunction
	public float GetLinearVelocityX(Body body) {
		return body.getLinearVelocity().x;
	}

	@SimpleFunction
	public float GetLinearVelocityY(Body body) {
		return body.getLinearVelocity().y;
	}

	@SimpleFunction
	public float GetAngularVelocity(Body body) {
		return body.getAngularVelocity();
	}

	@SimpleFunction
	public void SetAngularVelocity(Body body, float vel) {
		body.setAngularVelocity(vel);
	}

	@SimpleFunction
	public List GetAllBody(boolean onlyDynamic) {
		assertWorld();
		List<Body> result = new ArrayList<>();

		for (Body b = boxWorld.getWorld().getBodyList(); b != null; b = b.getNext()) {
			if (b.getType() == BodyType.STATIC) {
				if (!onlyDynamic) {
					result.add(b);
				}
			} else {
				result.add(b);
			}
		}
		return result;
	}

	@SimpleFunction
	public void SetActive(Body body, boolean active) {
		body.setActive(active);
	}

	@SimpleFunction
	public float GetCenterX(Body body) {
		return BoxUtil.M2P(body.getPosition().x);
	}

	@SimpleFunction
	public void SetCenterX(Body body, float X) {
		body.setTransform(new Vec2(BoxUtil.P2M(X), body.getPosition().y), body.getAngle());
		body.setActive(true);
	}

	@SimpleFunction
	public float GetCenterY(Body body) {
		return BoxUtil.M2P(body.getPosition().y);
	}

	@SimpleFunction
	public void SetCenterY(Body body, float Y) {
		body.setTransform(new Vec2(body.getPosition().x, BoxUtil.P2M(Y)), body.getAngle());

		body.setActive(true);
	}

	@SimpleFunction
	public float GetAngle(Body body) {
		return BoxUtil.radiansToDegrees(body.getAngle());
	}

	@SimpleFunction
	public void SetAngle(Body body, float angle) {
		body.setTransform(body.getPosition(), BoxUtil.degreesToRadians(angle));
		body.setActive(true);
	}

	@SimpleFunction
	public void SetBullet(Body body, boolean isBullet) {
		body.setBullet(isBullet);
	}

	@SimpleFunction
	public void SetFixedRotation(Body body, boolean isFixed) {
		body.setFixedRotation(isFixed);
	}

	@SimpleFunction
	public void SetGravityScale(Body body, float scale) {
		body.setGravityScale(scale);
	}

	@SimpleFunction
	public float GetMass(Body body) {
		return body.getMass();
	}

	@SimpleFunction
	public int GetType(Body body) {
		switch (body.getType()) {
		case DYNAMIC:
			return 1;
		case STATIC:
			return 2;
		case KINEMATIC:
			return 3;
		default:
			throw new YailRuntimeError("Unknown body type ", "GetType");
		}

	}

	@SimpleFunction
	public void SetType(Body body, int type) {
		switch (type) {
		case 1:
			body.setType(BodyType.DYNAMIC);
			break;
		case 2:
			body.setType(BodyType.STATIC);
			break;
		case 3:
			body.setType(BodyType.KINEMATIC);
			break;

		default:
			throw new YailRuntimeError("Unknown body type: " + type, "SetType");

		}
	}

	@SimpleFunction
	public boolean TestPoint(Body body, float x, float y) {
		return body.getFixtureList().testPoint(BoxUtil.pixelToVec2(x, y));
	}

	private Body createCircleBody(float centerX, float centerY, float radius, int type) {
		BodyDef bodyDef = new BodyDef();
		bodyDef.type = (type == 1 ? BodyType.DYNAMIC : (type == 2 ? BodyType.STATIC : BodyType.KINEMATIC));
		bodyDef.linearDamping = linearDamping;
		bodyDef.angularDamping = angularDamping;
		bodyDef.position.set(BoxUtil.pixelToVec2(centerX, centerY));

		CircleShape shape = new CircleShape();
		shape.setRadius(BoxUtil.P2M(radius));

		FixtureDef fixture = new FixtureDef();
		fixture.shape = shape;
		fixture.friction = friction;
		fixture.restitution = restitution;
		fixture.density = density;

		Body body = boxWorld.getWorld().createBody(bodyDef);
		body.createFixture(fixture);
		return body;
	}

	@SimpleProperty
	public void Friction(float friction) {
		if (friction >= 0) {
			this.friction = friction;
		}
	}

	@SimpleProperty
	public void Density(float density) {
		if (density >= 0) {
			this.density = density;
		}
	}

	@SimpleProperty
	public void Restitution(float restitution) {
		if (restitution >= 0) {
			this.restitution = restitution;
		}
	}

	@SimpleProperty
	public void LinearDamping(float linearDampling) {
		if (linearDampling >= 0) {
			this.linearDamping = linearDampling;
		}
	}

	@SimpleProperty
	public void AngularDamping(float angularDamping) {
		if (angularDamping >= 0) {
			this.angularDamping = angularDamping;
		}
	}

	@SimpleProperty
	public int TypeDynamic() {
		return 1;
	}

	@SimpleProperty
	public int TypeStatic() {
		return 2;
	}

	@SimpleProperty
	public int TypeKinematic() {
		return 3;
	}

	private Body createRectangleBody(int left, int top, int width, int height, int type) {
		BodyDef bodyDef = new BodyDef();
		bodyDef.type = (type == 1 ? BodyType.DYNAMIC : (type == 2 ? BodyType.STATIC : BodyType.KINEMATIC));
		bodyDef.linearDamping = linearDamping;
		bodyDef.angularDamping = angularDamping;
		bodyDef.position.set(BoxUtil.pixelToVec2(left + width / 2, top + height / 2));

		PolygonShape polygonShape = new PolygonShape();
		polygonShape.setAsBox(BoxUtil.P2M(width / 2), BoxUtil.P2M(height / 2));

		FixtureDef fixture = new FixtureDef();
		fixture.shape = polygonShape;
		fixture.friction = friction;
		fixture.restitution = restitution;
		fixture.density = density;

		Body body = boxWorld.getWorld().createBody(bodyDef);
		body.createFixture(fixture);
		return body;
	}

	private Body createPolygonBody(int centerX, int centerY, float[][] pts, float width, float height, int type) {
		BodyDef bodyDef = new BodyDef();
		bodyDef.type = (type == 1 ? BodyType.DYNAMIC : (type == 2 ? BodyType.STATIC : BodyType.KINEMATIC));
		bodyDef.linearDamping = linearDamping;
		bodyDef.angularDamping = angularDamping;
		bodyDef.position.set(BoxUtil.pixelToVec2(centerX, centerY));

		PolygonShape polygonShape = new PolygonShape();
		int count = pts.length;
		Vec2 vertices[] = new Vec2[count];
		for (int i = 0; i < vertices.length; i++) {
			// need to check this
			vertices[i] = BoxUtil.pixelToVec2(pts[i][0] - width / 2, pts[i][1] - height / 2);
		}
		polygonShape.set(vertices, count);

		FixtureDef fixture = new FixtureDef();
		fixture.shape = polygonShape;
		fixture.friction = friction;
		fixture.restitution = restitution;
		fixture.density = density;

		Body body = boxWorld.getWorld().createBody(bodyDef);
		body.createFixture(fixture);
		return body;
	}

	private void assertWorld() {
		if (boxWorld == null) {
			throw new RuntimeException("You need call SetBoxWorld before using Body ");
		}
	}
}
